Hibernate 7.4: пагинация с fetch join и встроенная поддержка аудита
Источник: https://blog.jetbrains.com/idea/2026/05/hibernate-7-4-new-features/
Краткое содержание
Hibernate 7.4 решает две давние проблемы JPA-приложений: неэффективную пагинацию при использовании fetch join с коллекциями и необходимость подключать внешние библиотеки для аудита изменений. Оба улучшения снижают объём инфраструктурного кода, который разработчики писали вручную.
Пагинация с fetch join: вложенные запросы вместо in-memory
До версии 7.4 запрос с join fetch и setMaxResults() не мог безопасно применять LIMIT на уровне базы данных: Hibernate загружал все строки и фильтровал уже в памяти приложения, что могло приводить к OutOfMemoryException на больших таблицах. Предупреждение HHH90003004 сигнализировало об этом поведении.
Hibernate 7.4 генерирует вложенный запрос: сначала выбирается ограниченный набор идентификаторов родительских сущностей, затем к ним дозагружаются коллекции:
-- Hibernate 7.4 генерирует:
SELECT o1_0.id, i1_0.order_id, i1_0.id, ...
FROM (
SELECT o1_0.id, o1_0.order_number, o1_0.status
FROM orders o1_0
WHERE EXISTS (SELECT 1 FROM order_items i1_0 WHERE o1_0.id = i1_0.order_id)
OFFSET ? ROWS FETCH FIRST ? ROWS ONLY
) o1_0(id, order_number, status)
JOIN order_items i1_0 ON o1_0.id = i1_0.order_id
Это позволяет пагинации происходить в базе данных, при этом возвращая полные коллекции для каждого выбранного родителя.
Исторические таблицы и аудит без Envers
Hibernate 7.4 добавляет нативную поддержку temporal-таблиц через аннотацию @Temporal, сохраняя предыдущие версии строк в отдельной таблице истории с колонками effective и superseded. Запрос состояния сущности на момент времени выглядит так:
Instant someTime = ...;
try (var session = sessionFactory.withOptions().asOf(someTime).open()) {
var product = session.find(Product.class, productId);
}
Для аудита теперь достаточно аннотации @Audited — без подключения Hibernate Envers:
@Entity
@Table(name = "products")
@Audited
@Audited.Table(name = "products_aud_log")
class Product { /* поля */ }
Таблица аудита фиксирует каждое изменение с типом операции через enum ModificationType (ADD=0, MOD=1, DEL=2).
Значимость
Hibernate 7.4 переводит на уровень фреймворка две задачи, которые ранее требовали либо ручного написания кастомных запросов, либо подключения дополнительных зависимостей. Это уменьшает поверхность кода, которую нужно поддерживать, и устраняет целый класс производительностных ловушек при работе с коллекциями в JPA.
🧾 Транскрипт (формат)
Hibernate 7.4 New Features Source: https://blog.jetbrains.com/idea/2026/05/hibernate-7-4-new-features/
Hibernate 7.4 introduced several improvements that simplify loading a page of data along with their associated child collection, historical data access, and audit logging.
The article will focus on the following features:
Limits and Fetch Joins: How Hibernate 7.4 improves working with paginated queries that include fetched associations. History and Audit Tables: How the new capabilities support querying entity state across time and working with historical data.
You can check out the sample code for this article in this GitHub repository.
Limits and Fetch Joins One common requirement in data-driven applications is loading a page of parent entities along with an associated child entity collection. For example, suppose an application has an Order entity with a Set<OrderItem> collection, and we want to load the first few orders together with their order items.
List<Order> orders = session .createSelectionQuery( "select o from Order o join fetch o.items order by o.id", Order.class ) .setMaxResults(10) .getResultList(); In Hibernate versions before 7.4, applying a limit to a query that used a collection fetch join could not be safely pushed down to the database. Because each Order may have multiple OrderItem rows, limiting the SQL result directly could cut off part of an order’s item collection. To avoid returning incomplete collections, Hibernate loaded all matching rows from the database and applied pagination in memory at the application layer.
That behavior was correct, but it could be expensive. A query intended to load only 10 orders might still read many more rows if the table contained a large number of orders and order items.
Before Hibernate 7.4, the generated SQL would look like the following:
select o1_0.id, i1_0.order_id, i1_0.id, i1_0.product_code, i1_0.quantity, o1_0.order_number, o1_0.status from orders o1_0 join order_items i1_0 on o1_0.id=i1_0.order_id As you can see, the limit(pagination) is not applied at the SQL query level. So, it will load all the orders and their associated order_items, which could be a very expensive operation and may result in OutOfMemoryException.
You can see a WARNING logged by Hibernate as follows:
[WARN] HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory One option to prevent Hibernate performing pagination in memory is by setting the following property:
hibernate.query.fail_on_pagination_over_collection_fetch=true By configuring this property, Hibernate throws an exception instead of performing pagination in memory.
Hibernate 7.4 fixes this problem by using nested queries. Instead of applying the limit directly to the joined result set, Hibernate first determines the limited set of parent entity identifiers and then fetches the associated collection for only those parent rows.
This allows pagination to happen in the database while still returning complete items collections for each selected Order.
With Hibernate 7.4, the SQL will be generated as follows:
select o1_0.id, i1_0.order_id, i1_0.id, i1_0.product_code, i1_0.quantity, o1_0.order_number,o1_0.status from (select o1_0.id, o1_0.order_number, o1_0.status from orders o1_0 where exists(select 1 from order_items i1_0 where o1_0.id=i1_0.order_id) offset ? rows fetch first ? rows only) o1_0(id, order_number, status) join order_items i1_0 on o1_0.id=i1_0.order_id This improvement makes fetch joins more practical for paginated screens, such as an order listing page that displays each order with its line items, without forcing the application to load the full result set first.
History and Audit Tables Hibernate 7.4 adds built-in support for temporal history tables and audit tables. Both features help track changes to entity data, but they serve slightly different use cases: history tables let us query the state of an entity at a point in time, while audit tables record the sequence of changes that happened to an entity.
Consider the following Product entity:
@Entity @Table(name = "products") class Product { //fields id, code, name, price } History Tables To enable temporal history for Product, annotate the entity with @Temporal and optionally specify the history table name using @Temporal.HistoryTable.
@Entity @Table(name = "products") @Temporal @Temporal.HistoryTable(name="products_history") class Product { //fields id, code, name, price } With this mapping, Hibernate stores previous versions of product rows in the products_history table. The table includes the entity columns plus two temporal columns: effective, which marks when a version became active, and superseded, which marks when that version was replaced.
products_history table:
id code name price effective superseded 2251 P1000 Product-1000 40.00 2026-05-15 08:21:39.949001 +00:00 null 2301 P1001 Product-1001 90.00 2026-05-15 08:22:24.765883 +00:00 2026-05-15 08:22:24.778067 +00:00 2301 P1001 Product-1001 100.00 2026-05-15 08:22:24.778067 +00:00 null We can get the Product entity data at a given point of time as follows:
Instant someTime = ... try (var session = sessionFactory.withOptions().asOf(someTime).open()) { var product = session.find(Product.class, productId);
} This makes temporal queries feel like normal entity lookups while Hibernate resolves the correct historical row behind the scenes.
Hibernate offers several different strategies(NATIVE, SINGLE_TABLE, HISTORY_TABLE) for mapping temporal entities. For more info check out the Temporal data section.
Audit Tables Previously, Hibernate-based applications typically used the separate Hibernate Envers library for auditing entity changes. Hibernate 7.4 brings audit table support into Hibernate ORM itself, so applications can use auditing features natively without adding Envers for this use case.
Audit support is enabled by adding @Audited and can be mapped to a custom table using @Audited.Table.
@Entity @Table(name = "products") @Audited @Audited.Table(name="products_aud_log") class Product { //fields id, code, name, price } When auditing is enabled, Hibernate writes one row per change into the audit table. Unlike the history table, the audit table focuses on recording what operation happened and when.
id code name price rev revtype 2001 P1002 Product-1002 90.00 2026-05-13 14:58:17.505775 +00:00 0 2001 P1002 Product-1002 100.00 2026-05-13 14:58:17.518194 +00:00 1 The rev values are the timestamps at which the change happened. The revtype values are represented using ModificationType enum as follows:
public enum ModificationType { /** * Creation, encoded as 0 */ ADD, /** * Modification, encoded as 1 */ MOD, /** * Deletion, encoded as 2 */ DEL } For more info check out the Audit logs section.
Summary Most of the applications use pagination to show a list of resources, and we used to write custom logic to load paginated data along with the associated child collection. Now this is being handled at the framework level itself. Also, we used to rely on external libraries like Envers to implement auditing, which is now provided by Hibernate itself.
Hibernate 7.4 brings practical improvements that address real problems in JPA/ Hibernate-based applications. Whether we are optimizing pagination query behavior or tracking historical data, Hibernate 7.4 reduces the amount of custom infrastructure needed and provides better support out of the box without requiring additional libraries.
Go ahead and explore these new features using this GitHub repository.